package uz.rt.dicom.io;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The binary input stream creates an extension to the FilterInputStream by adding the concept of Little and Big
 * Endian.
 * The binary input stream can read in Little and Big Endian:
 * <ul>
 * <li>a byte</li>
 * <li>an array bytes</li>
 * <li>signed and unsigned integers, floats, doubles</li>
 * <li>string</li>
 * </ul>
 */
public class BinaryInputStream extends FilterInputStream
{
// ------------------------------ FIELDS ------------------------------

    /** BinaryInputStream logger */
    private final static Logger logger = Logger.getLogger(BinaryInputStream.class.getName());
    /** Current offset position in the stream */
    int position;
    /** Mark a position in the stream */
    int mark;
    /** Byte buffer storing input stream data. */
    private byte[] buffer;

    /** Byte order which is used to read data input stream. */
    private ByteOrder byteOrder;

// --------------------------- CONSTRUCTORS ---------------------------

    /**
     * Creates a binary input stream using the default byte order (Little Endian)(@see ByteOrder)
     *
     * @param in Input stream
     */
    protected BinaryInputStream(InputStream in)
    {
        this(in, ByteOrder.LITTLE_ENDIAN);
        position = 0;
        mark = 0;
    }

    /**
     * Creates a binary input stream and specifies the byte order.
     *
     * @param in        Input stream
     * @param byteOrder Byte order which is used to read data input stream
     */
    protected BinaryInputStream(InputStream in, ByteOrder byteOrder)
    {
        super(in);
        this.byteOrder = byteOrder;
        buffer = new byte[8];
        position = 0;
        mark = 0;
    }

// --------------------- GETTER / SETTER METHODS ---------------------

    /**
     * Get the byte order which is used to read data input stream.
     *
     * @return Little or Big Endian byte order
     */
    public ByteOrder getByteOrder()
    {
        return byteOrder;
    }

    /**
     * Set the byte order which is used to read data input stream.
     *
     * @param byteOrder Little or Big Endian byte order
     */
    public void setByteOrder(ByteOrder byteOrder)
    {
        this.byteOrder = byteOrder;
    }

// -------------------------- OTHER METHODS --------------------------

    public boolean endOfStream() throws IOException
    {
        if (!markSupported())
        {
            logger.log(Level.WARNING,
                       "Unable to detect end of stream because markSupported()==false. DicomInputStream.endOfStream() will return false.");
            return false;
        }
        super.mark(1);
        int r = super.read();
        super.reset();
        return (r == -1);
    }

    @Override
    public boolean markSupported()
    {
        return super.markSupported();
    }

    @Override
    public synchronized void mark(int readlimit)
    {
        super.mark(readlimit);
        mark = position;
    }

    @Override
    public synchronized int read() throws IOException
    {
        int b = super.read();
        if (b >= 0)
            position++;
        return b;
    }

    @Override
    public synchronized int read(byte[] b) throws IOException
    {
        int n = super.read(b);
        position += n;
        return n;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException
    {
        int n = super.read(b, off, len);
        position += n;
        return n;
    }

    /**
     * Reads a byte and checks if the value is 1 or 0.
     *
     * @return True if the byte is not equal to 0, else false
     * @throws IOException If an IO error occurs.
     */
    public synchronized boolean readBoolean() throws IOException
    {
        boolean b = in.read() != 0;
        position++;
        return b;
    }

    /**
     * Reads one byte from the input stream
     *
     * @return A byte read from the input stream
     * @throws IOException If an IO error occurs.
     */
    public synchronized byte readByte() throws IOException
    {
        byte b = (byte) in.read();
        position++;
        return b;
    }

    /**
     * Reads a 64 bit double value.
     *
     * @return Double value
     * @throws IOException If an IO error occurs.
     */
    public synchronized double readDouble() throws IOException
    {
        if (readToBuffer(buffer, 0, 8) != 8)
            throw new IOException("Unable to read 8 bytes from stream to a buffer.");
        return Double.longBitsToDouble(buffer2Unsigned64());
    }

    /**
     * Read a number of bytes from the input stream starting.
     *
     * @param b      Byte array storing the data read from the input stream
     * @param offset Offset to the current position in the input stream from where the reading process starts.
     * @param length Number of bytes that needs to read from the input stream.
     * @return Number of bytes that is read from the input stream.
     * @throws IOException If an IO error occurs.
     */
    public synchronized int readToBuffer(byte[] b, int offset, int length) throws IOException
    {
        int remaining = length;
        int received = 0;
        while (remaining > 0)
        {
            int n = in.read(b, offset, remaining);
            if (n == -1)
                throw new IOException("Reached the end of the inputstream." + System.getProperty("line.separator")
                                              + "\tNumber of bytes read: " + received + "." +
                                              System.getProperty("line.separator")
                                              + "\tNumber of bytes remaining: " + remaining + ".");
            remaining -= n;
            offset += n;
            position += n;
            received += n;
        }
        return received;
    }

    /**
     * Convert the buffer to an unsigned 64 bit value
     *
     * @return Unsigned 64 bit value
     */
    private synchronized long buffer2Unsigned64()
    {
        long a = (long) (buffer[0] & 0xff);
        long b = (long) (buffer[1] & 0xff);
        long c = (long) (buffer[2] & 0xff);
        long d = (long) (buffer[3] & 0xff);
        long k = (long) (buffer[4] & 0xff);
        long l = (long) (buffer[5] & 0xff);
        long m = (long) (buffer[6] & 0xff);
        long n = (long) (buffer[7] & 0xff);
        return (byteOrder == ByteOrder.BIG_ENDIAN) ?
                ((((((a << 8 | b) << 8 | c) << 8 | d) << 8 | k) << 8 | l) << 8 | m) << 8 | n :
                ((((((n << 8 | m) << 8 | l) << 8 | k) << 8 | d) << 8 | c) << 8 | b) << 8 | a;
    }

    /**
     * Read a float value.
     *
     * @return Float value
     * @throws IOException If an IO error occurs.
     */
    public synchronized float readFloat() throws IOException
    {
        if (readToBuffer(buffer, 0, 4) != 4)
            throw new IOException("Unable to read 4 bytes from stream to a buffer.");
        return Float.intBitsToFloat(buffer2Signed32());
    }

    /**
     * Convert the buffer to a signed 32 bit value
     *
     * @return Signed 32 bit value
     */
    public synchronized final int buffer2Signed32()
    {
        int a = buffer[0] & 0xff;
        int b = buffer[1] & 0xff;
        int c = buffer[2] & 0xff;
        int d = buffer[3] & 0xff;
        return (byteOrder == ByteOrder.BIG_ENDIAN) ?
                ((a << 8 | b) << 8 | c) << 8 | d :
                ((d << 8 | c) << 8 | b) << 8 | a;
    }

    /**
     * Read a signed 16 bit value.
     *
     * @return Signed 16 bit value
     * @throws IOException If an IO error occurs.
     */
    public synchronized int readSigned16() throws IOException
    {
        if (readToBuffer(buffer, 0, 2) != 2)
            throw new IOException("Unable to read 2 bytes from stream to a buffer.");
        return buffer2Signed16();
    }

    /**
     * Convert the buffer to a signed 16 bit value
     *
     * @return Signed 16 bit value
     */
    public synchronized short buffer2Signed16()
    {
        short a = (short) (buffer[0] & 0xff);
        short b = (short) (buffer[1] & 0xff);
        return (short) ((byteOrder == ByteOrder.BIG_ENDIAN) ? a << 8 | b : b << 8 | a);
    }

    /**
     * Read a signed 32 bit value.
     *
     * @return Signed 32 bit value
     * @throws IOException If an IO error occurs.
     */
    public synchronized int readSigned32() throws IOException
    {
        if (readToBuffer(buffer, 0, 4) != 4)
            throw new IOException("Unable to read 4 bytes from stream to a buffer.");
        return buffer2Signed32();
    }

    /**
     * Read a string from the input stream and define the character encoding set to ASCII.
     *
     * @param length Length in bytes of the String
     * @return String read from the input stream.
     * @throws IOException If an IO error occurs.
     */
    public synchronized String readString(int length) throws IOException
    {
        return readString(length, "ASCII");
    }

    /**
     * Read a string from the input stream and define the character encoding set.
     *
     * @param length  Length in bytes of the String
     * @param charset Character encoding set
     * @return String read from the input stream.
     * @throws IOException If an IO error occurs.
     */
    public synchronized String readString(int length, String charset) throws IOException
    {
        byte bytes[] = new byte[length];
        int lread = in.read(bytes);
        position += lread;
        if (lread != length)
            throw new IOException("Unable to read the String with a length of " + length + " bytes");
        return new String(bytes, charset);
    }

    /**
     * Read an unsigned 16 bit value
     *
     * @return Unsigned 16 bit value
     * @throws IOException If an IO error occurs.
     */
    public synchronized int readUnsigned16() throws IOException
    {
        if (readToBuffer(buffer, 0, 2) != 2)
            throw new IOException("Unable to read 2 bytes from stream to a buffer.");
        return buffer2Unsigned16();
    }

    /**
     * Convert the buffer to an unsigned 16 bit value
     *
     * @return Unsigned 16 bit value
     */
    public synchronized final int buffer2Unsigned16()
    {
        int a = buffer[0] & 0xff;
        int b = buffer[1] & 0xff;
        return (byteOrder == ByteOrder.BIG_ENDIAN) ? a << 8 | b : b << 8 | a;
    }

    /**
     * Read an unsigned 32 bit value
     *
     * @return Unsigned 32 bit value
     * @throws IOException If an IO error occurs.
     */
    public synchronized long readUnsigned32() throws IOException
    {
        if (readToBuffer(buffer, 0, 4) != 4)
            throw new IOException("Unable to read 4 bytes from stream to a buffer.");
        return buffer2Unsigned32();
    }

    /**
     * Convert the buffer to an unsigned 32 bit value
     *
     * @return Unsigned 32 bit value
     */
    public synchronized long buffer2Unsigned32()
    {
        long a = (long) (buffer[0] & 0xff);
        long b = (long) (buffer[1] & 0xff);
        long c = (long) (buffer[2] & 0xff);
        long d = (long) (buffer[3] & 0xff);
        return (byteOrder == ByteOrder.BIG_ENDIAN) ?
                ((a << 8 | b) << 8 | c) << 8 | d :
                ((d << 8 | c) << 8 | b) << 8 | a;
    }

    /**
     * Read an unsigned 8 bit value
     *
     * @return Unsigned 8 bit value
     * @throws IOException If an IO error occurs.
     */
    public synchronized int readUnsigned8() throws IOException
    {
        if (readToBuffer(buffer, 0, 1) != 1)
            throw new IOException("Unable to read 1 byte from stream to a buffer.");
        return buffer2Unsigned8();
    }

    /**
     * Convert the buffer to an unsigned 8 bit value
     *
     * @return Unsigned 8 bit value
     */
    public synchronized final int buffer2Unsigned8()
    {
        return buffer[0] & 0xff;
    }

    @Override
    public synchronized void reset() throws IOException
    {
        super.reset();
        position = mark;
    }

    @Override
    public synchronized long skip(long skip) throws IOException
    {
        long n = super.skip(skip);
        if (n > 0)
            position += n;
        return n;
    }
}
